1    ' a test file of basic code - some with line numbers, some without.
90   '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
100  ' renumber.bas
     ' Geoff Graham 2010    http://geoffg.net
     ' updated by James Deakins February 2013 to use newer functions 
     ' of MMBasic V4.3
140  '
150  ' This program will renumber a basic program including references
160  ' to line numbers in statements.  It is written for MMBasic, a version
165  ' of GW-Basic with enhancements to support modern programming and 
166  ' external devices.
170  '
180  ' For comparison, Geoff's original program was 218 lines.  This is
190  ' around 1000 lines.
200  '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
210  '
215 Start:
Beginning: GoTo 100 ' a temporary test to check line number/s updated
220  Option Base 1 ' Arrays start with entry 1, not 0
230  '
240  false = 0 : true = NOT false
250  bDebug = True
260  defaultStart = 1 : defaultFirst = 10 : defaultIncrement = 10
261  '
262  ' leave the Config file until 2 reported bugs fixed.
265  ' ReadConfigFile()   ' read in user preferences
270  ' 
280  if bDebug then
290    open "Debug.txt" for Output as #9
300  endif
310  '
320  MaxParams = 20
330  DIM Param$(MaxParams)   ' the parameters after a keyword 
340  DIM ParamPtr(MaxParams) ' the pointer position of start of parameters 
350  '
360  DATA "THEN","GOTO","GOSUB","ELSE","SETPIN","SETTICK" 
370  KeyWords = 6  ' update this if you change the list of keywords.
380  DIM KeyWord$(KeyWords)
390  For n = 1 to KeyWords
400    Read kw$
410    KeyWord$(n) = kw$
420  Next n
430  '
440  PRINT "This program will renumber a BASIC program including references"
450  PRINT "to line numbers in GOSUB, GOTO, etc.  The error checking is"
460  PRINT "far from perfect, so the program must run correctly before"
470  PRINT "renumbering."
480  PRINT
490  INPUT "Program to renumber: ", iname$
500  If iname$ = "" then iname$ = "testfile.bas"
510  LinesInFile = CountLines(iname$) 
520  DIM lines(LinesInFile, 3)  ' first entry is the line number extracted from each line, second is new line number, 
525  '                          ' third is flag to show whether it is the target of a keyword.
530  DIM DQuotes(LinesInFile)   ' holds the number of double quotes on each line
540  DIM Remarks(LinesinFile, 2)' first entry is posn of first single quote on each line, second is posn of first REM
550  ' DIM labelLines(LinesInFile)       'TODO use it
560 
570  OPEN iname$ FOR input  AS #1 
580  '
590  INPUT "   Output file name: ", oname$
600  If oname$ = "" then oname$ = "renum-ed.bas"
610  OPEN oname$ FOR output AS #2
620  '
     ' 270 PRINT : PRINT "In the input file, what is the desired:"
     ' 275 PRINT "(Default"; defaultStart; ")"; :     INPUT "  Line num where renumbering is to start (Enter key for default)"; In$
     ' 280 If In$ = "" then start = defaultStart else start = VAL(In$)
660  PRINT : PRINT "In the output file, what is the desired:"
670  PRINT "(Default"; defaultFirst; ")"; :     INPUT " First num to be used in the new sequence (Enter key for default)"; In$
680  If In$ = "" then first = defaultFirst else first = VAL(In$)
690  PRINT "(Default"; defaultIncrement; ")"; : INPUT "                 Increment for each line (Enter key for default)"; In$
700  If In$ = "" then increment = defaultIncrement else increment = VAL(In$)
710  '
720  'PRINT: INPUT "Do you want line numbers to be inserted on those lines that currently do not have line numbers (Y/N)"; In$
725  'bInsertLineNumbersWhenBlank = (UCase$(In$) = "Y") ' in final program this will be set based on value in config file
726  bInsertLineNumbersWhenBlank = True
730   
732  'if (NOT bInsertLineNumbersWhenBlank) then
735  '  PRINT: INPUT "Do you want line numbers to increment over lines without line numbers (Y/N)"; In$
740  '  bIncrementOnBlank = (UCASE$(In$) = "Y")   ' in final program this will be set based on value in config file
745  'else
746  '  bIncrementOnBlank = False
747  'endif
748  bIncrementOnBlank = True 'TODO When bugs fixed, move these questions into the Config File setup
750  '
760  '
770  '
780  'Pass one of the file - store the line numbers as integers in lines array
790  '-Warn the user if any lines are out of order (sometimes not obvious, and a cause of weird errors!)
800  '-Warn if any old style remarks signified by REM are used.
810  ' TODO Identify any labels and record the length of the longest one.
820  PRINT : PRINT "Checking for some potential problems.... "
830  '--------------------------
840  PrevNum = 0
850  bLinesInOrder = True
860  bREMstatements = False
865  NextNewLineNum = first
870  FOR LineNum = 1 to LinesInFile 
880    Line INPUT #1, inp$
890    ' store the line number
900    ThisNum = VAL(inp$)
910    lines(LineNum, 1) = ThisNum
911    if ThisNum > 0 then  ' not a label
912      lines(LineNum, 2) = NextNewLineNum
913      NextNewLineNum = NextNewLineNum + increment
914    endif                ' no need to store zero for labels - already initialised to zero.
920    ' Check for lines out of order
930    If (ThisNum <= PrevNum) AND (ThisNum <> 0) Then ' a blank line number (allowable) will return zero
940      PRINT "File line"; LineNum; " has its program linenumber ("; ThisNum; " ) out of order!"
950      bLinesInOrder = False
960    Endif
970    ' Check how many double quotes (may hold REM, single quote or colon which are all searched for further down in program)
980    DQCount = 0
990    DQStart = INSTR(1, inp$, chr$(34)) ' search for the next double quote    
1000   DO While DQStart > 0)              ' line 1000 in source
1010     DQCount = DQCount + 1
1020     DQStart = INSTR(DQStart + 1, inp$, chr$(34))
1030   LOOP
1040   DQuotes(LineNum) = DQCount
     
1060   ' Check for REM  
1070   UCLine$ = UCASE$(inp$)
     
           ' check for a single quote, because we only care about REM if it's BEFORE a single quote
           SQPosn = INSTR(1, UCLine$, "'")
           bSQFound = False
           DO While (SQPosn > 0) and (NOT bSQFound)
             if NOT InDoubleQuotes(SQPosn, UCLine$) then ' make sure it isn't in double quotes, like "Henry's"
                bSQFound = True
             else
                SQPosn = INSTR(SQPosn + 1, UCLine$, "'")
             endif
           Loop 
           Remarks(LineNum,1) = SQPosn
           if SQPosn = 0 THEN
              SQPosn = LEN(UCLine$) ' if there isn't a single quote, assume one at the far right of the line 
           endif
           
1240   RemPosn = GetNextREM(1, UCLine$)
1250   DO While (RemPosn > 0) AND (NOT bREMstatements) AND (RemPosn < SQPosn) ' only search up to the single quote
1260     if DQCount > 0 then
1270       if NOT InDoubleQuotes(RemPosn, UCLine$) then
1280         bREMstatements = True
1290       endif
         else ' no double quotes in line
           bREMstatements = True 
1320     endif
         if NOT bREMstatements then
1340       RemPosn = GetNextREM(RemPosn + 1, UCLine$)
         endif
1360   LOOP   
       Remarks(LineNum, 2) = RemPosn  
1380   if Thisnum > 0 AND bLinesInOrder then ' don't bother updating if it was a label, or if lines out of order
1390     PrevNum = ThisNum
       endif
1410 Next LineNum
1420 CLOSE #1
1430 '
1440 '
     ' Get the length of the max line number so we can get neat spacing
     if bIncrementOnBlank then
       MaxLineNum = first + ((LineNum - 1) * increment)
     else
       LineNum = LinesInFile
       bExit = False 
       Do While (NOT bExit) AND (LineNum > 0) 
         MaxLineNum = Lines(LineNum, 2)
         if MaxLineNum > 0 then  
           bExit = True
         else
           LineNum = LineNum - 1 
         endif
       Loop   
     endif
     ' Note: if no line numbers in the program at all MaxLineNum will be zero, so no need for padding for alignment
     if MaxLineNum = 0 then 
       MaxLineNumLen = 0
     else
       MaxLineNumLen = LEN(Str$(MaxLineNum))
     endif
     
     if bDebug then    
       FOR LineNum = 1 to LinesInFile
         ? #9, "LineNum is"; LineNum; " Prog Line Num is "; lines(Linenum,1); " New Line Num is "; lines(Linenum, 2)
       next Linenum
       if bIncrementOnBlank then
         ? #9, "Increment on Label is True.  "
       else
         ? #9, "Increment on Label is False. "
       endif
       ? #9, "MaxLineNum is";MaxLineNum; " and its length is";MaxLineNumLen
     endif

1450 if NOT bLinesInOrder Then
       ?
1470   ? "Duplicate line numbers were found, or lines are out of order."
1480   ? "Only the first duplicate or out-of-order line number of a set is"
1490   ? "identified, so also check the line numbers below the ones identified."
1500   ?
1510   ? "You should only continue with the renumbering process if you are"
1520   ? "really sure of what you are doing!"
1530   ?
1540   Input "Line Number errors found.  Continue with Renumbering (Y/N)"; In$
1550   if Ucase$(In$) = "N" then END
1560 endif
1570 '
     if bREMstatements Then
       ?
       ? "WARNING: Old style REM statements were found (REM is the "
       ? "older way to signifying remarks).  This program identifies "
       ? "REM statements by searching for 'REM ', so be careful using "
       ? "variable names ending in rem, like Harem, Spirem, Theorem "
       ? "or Millirem in your program. "
       ? "Although the program has been written to account for the "
       ? "various ways the REM string might appear in your program, "
       ? "it is not guaranteed to work in all cases. "
       ? "If possible, change the variable names to not end in rem. "      
       ? "If you decide to continue with the renumbering process "
       ? "without changing the variable names, check the result carefully. "
       ?
       INPUT "Do you want to continue (Y/N)"; In$
       if Ucase$(In$) = "N" Then END
     endif
         
              
1760 'Pass 2 of the file - renumber to output file
1770 ' -----------------
1780 PRINT : PRINT "Writing to Output File: "
1790 OPEN iname$ FOR INPUT AS #1  
1800 FOR lineindex = 1 TO LinesInFile  
1810   LINE INPUT #1, inp$ 
1820   if bDebug Then ? #9: ? #9, "Original line is             >>>";inp$;"<<<"
1830   ProcessedLine$ = ProcessLine$(inp$, lineindex)
1840   PRINT #2, ProcessedLine$    ' send to Output file 
1850   PRINT ".";  
1860 NEXT lineindex   
1870 '  
1880 CLOSE #1, #2
1890 if bDebug then CLOSE #9
1900 PRINT  
1910 PRINT "Finished.  Processed";LinesInFile;" lines"  
1920 END  
1930 '  
1940 '
1950 '
1960 '  
1970 ''''''''''''''''''''''''''''''''''''''''''''  
1980 'SUBROUTINES and FUNCTIONS 
1990 ''''''''''''''''''''''''''''''''''''''''''''  
2000 '  
2010 '  
2020 '  
2030 ''''''''''''''''''''''''''''''''''''''''''''  
2040 'Function  
2050 'process a line and replace all line numbers.
2060 'Parameters: pInp$ - the line to be modified
2070 '            pLineIndex - the line counter from
2080 '                         the input file
2090 'Returns the modified line  
2100 ''''''''''''''''''''''''''''''''''''''''''''
Function ProcessLine$(pInp$, pLineIndex)  
Local original$, newptr
'
original$ = pInp$ 
ptr = 1
 
' move pointer past any leading spaces
ptr = MovePtrPastSpaces(pInp$, ptr)
 
' move pointer past line number
ptr = MovePtrPastLineNumber(pInp$, ptr, LineNum) ' linenumber from code line is returned in LineNum
' UPTOHERE *************************************************************************************************************************************************************************************
'insert the new line number.  

if bIncrementOnBlank then
  NewLineNum = first +((pLineIndex-1)*increment)
else
  NewLineNum = lines(pLineIndex, 2)
endif
if bDebug then ? #9, "^^ pLineIndex is"; pLineIndex; " and NewLineNum is "; NewLineNum
NewLineNumLength = len(str$(NewLineNum)) 
Indent$ = space$(MaxLineNumLen - NewLineNumLength)

If lines(pLineIndex, 1) > 0 THEN ' it has a line number
' TODO test for "Start Line" will go here - If lines(pLineIndex,1) < start then use original line number with original code etc, but adjust indent.
  newline$ = STR$(NewLineNum)+ Indent$ + MID$(inp$, ptr)
Else ' no line number
  n = INSTR(ptr, inp$, ":")
  if n > 0 then ' could be a label without a line number
    bLabel = True
    for i = ptr to n 
      ch$ = MID$(inp$, i, 1)
      if (ch$ = " ") then bLabel = False ' a label is a string immediately followed by a colon, so cannot have a space
    next i
    if bLabel then
      newLine$ = mid$(inp$, ptr) ' output with no indent (ie, at start of line)
    else   ' ? "colon in multi statement.  Not a label.
      if bInsertLineNumbersWhenBlank then
        newline$ = STR$(NewLineNum) + Indent$ + " " + MID$(inp$, ptr)
      else   
        newline$ = space$(MaxLineNumLen + ptr) + MID$(inp$, ptr)  
      endif
    endif
  else ' ? "no colon, no line number" 
    if bInsertLineNumbersWhenBlank then
      if bDebug then ? #9, "bInsertLineNumbersWhenBlank is True and NewLineNum is";NewLineNum; " and Indent is >>>";Indent$;"<<<"
      newline$ = STR$(NewLineNum) + space$(MaxLineNumLen + ptr - NewLineNumLength) + MID$(inp$, ptr)
    else
      if bDebug then ? #9, "bInsertLineNumbersWhenBlank is False and NewLineNum is";NewLineNum; " and Indent is >>>";Indent$;"<<<"
      newline$ = space$(MaxLineNumLen + ptr) + MID$(inp$, ptr)  
    endif
  endif
Endif  
 
if bDebug then ? #9, "after new line num added     >>>";newline$;"<<<"
 
' we leave the code above with the (possibly new) linenumber attached to the code and remarks.
 
UCnewline$ = UCASE$(newline$)
' A line can be made up of code and remarks.  Don't search for keywords in remarks.
' Separate the remarks from the code, and just process the code from now on.
' We will reattach the remarks after the code is processed.
 
' In pass one we stored pointers to the first ' and REM in the line that weren't in double quotes
 
' look for the start of remarks. Could be a single quote (') or REM.  If both, use the first one on the line.
 
ptrSingleQuote = Remarks(pLineIndex, 1)
ptrREM         = Remarks(pLineIndex, 2) 
 
ptrRemStart = ptrSingleQuote  'ptrRemStart will hold the earliest Start Of Remarks address, whether ' or REM
 
if (ptrREM > 0) then
  if ptrSingleQuote = 0 then 
    ptrRemStart = ptrREM
  elseif (ptrREM < ptrSingleQuote) Then
    ptrRemStart = ptrREM
  endif
endif
 
' Split the line into code and comments
 
If ptrRemStart > 0 Then
  LenDiff = LEN(newline$) - LEN(pInp$) ' we've probably added spacing to the beginning of the line
  ptrRemStart = ptrRemStart + LenDiff
  Remarks$ = MID$(newline$, ptrRemStart)
  Code$ = Left$(newline$, ptrRemStart - 1)
else  'it is all code 
  Remarks$ = ""
  Code$ = newline$ 
endif
 
if bDebug Then
  ? #9, "ptrSQ is  >>>";ptrSingleQuote;"<<<"
  ? #9, "ptrREM is >>>";ptrREM;"<<<"
  ? #9, "LenDiff   >>>";LenDiff;"<<<"
  ? #9, "Rem Start >>>";ptrRemStart;"<<<"
  ? #9, "Code is   >>>";Code$;"<<<"
  ? #9, "Rem  is   >>>";Remarks$;"<<<"
endif
 
' A code line may be made up of multiple statements separated by colons (:)
' Process each statement separately, and reassemble.
 
startOfStatement = 1
ProcessedLine$ = ""
bValidColon = False
 
if bDebug then
  ? #9, "ProcessLine: Searching for valid colons in code line: ";Code$
endif
 
DO 
    nextColon = INSTR(startOfStatement, Code$, ":")
    if bDebug then ? #9, "NextColon is"; nextColon; " and bValidColon is"; bValidColon
    DO WHILE NOT (bValidColon) AND (nextColon > 0)       
        if NOT InDoubleQuotes(nextColon, Code$) then ' only OK if it's not in double quotes
            bValidColon = True
        else
           nextColon = INSTR(nextColon + 1, Code$, ":")
        endif
    LOOP
       
    if nextColon = 0 then 
      endOfStatement = len(Code$)
    else
      endOfStatement = nextColon
    endif
    ' extract next statement and process it
    Statement$ = Mid$(Code$, startOfStatement, endOfStatement - startOfStatement + 1)
    If bDebug Then ? #9, "Processing Statement >>>";Statement$;"<<<"
    ProcessedLine$ = ProcessedLine$ + ProcessStatement$(Statement$)
    if bDebug Then ? #9, "New Line is now              >>>";ProcessedLine$;"<<<"
      startOfStatement = nextColon + 1 
Loop Until nextColon = 0
 
ProcessedLine$ = ProcessedLine$ + Remarks$   ' reattach remarks
if bDebug Then ? #9, "And with Remarks reattached  >>>";ProcessedLine$;"<<<"
ProcessLine$ = ProcessedLine$
 
End Function ' ProcessLine$
'
'''''''''''''''''''''''''''''''''''''''''''''''
' Function
' Process the statement
' Return processed statement
'''''''''''''''''''''''''''''''''''''''''''''''
' Note: the original code by Geoff had separate
' processing for the ON command. The ON processing
' has now been rolled into the same code as GOTO
' and GOSUB.
' GOTO and GOSUB will handle both a single line
' number or label, or multiple ones separated by 
' commas.
'''''''''''''''''''''''''''''''''''''''''''''''
'
Function ProcessStatement$(pStatement$)
' NOTE: The newStatement$ will be a different length to pStatement$ if the new 
'       line numbers embedded in it are different lengths 
 
Local i, newStatement$, UCstatement$, kwPtr ' points to posn of keyword in statement
 
bProcessedKeyword = false
newStatement$ = pStatement$
 
for i = 1 to KeyWords
  kw$ = KeyWord$(i)
  kwPtr = 1
  DO WHILE kwPtr <> 0
    kwPtr = INSTR(kwPtr, Ucase$(newStatement$), kw$)     ' find the keyword
    if bDebug then ? #9, "Search for keyword ";kw$; space$(7-len(kw$)); " returned kwPtr";kwptr; " (0 means not found)"
    IF kwPtr <> 0 THEN  
      IF NOT InDoubleQuotes(kwPtr, newStatement$) Then
        kwPtr = kwPtr + LEN(kw$)              'step over it.  pointer at char after end of keyword.
        newStatement$ = ProcessKeyWord$(kwPtr, newStatement$, kw$)
        bProcessedKeyword = True'
        if bDebug then ? #9, "kwPtr returned was ";kwPtr
      ELSE 'it is in double quotes, so do not process it as a keyword
        kwPtr = kwPtr + LEN(kw$)              'step over it
      ENDIF
    ENDIF  
  LOOP 
Next i  
 
If bProcessedKeyword = False then ' no keywords found
  ' "No Keywords found.  Leave as is"
  newStatement$ = pStatement$ 
endif
 
ProcessStatement$ = newStatement$
End Function  ' ProcessStatement$      
'
'''''''''''''''''''''''''''''''''''''''''''''
' Function
' Function is passed a pointer to where the
' nominated keyword is in the nominated statement.
' Return processed statement and revised pointer.
'
'''''''''''''''''''''''''''''''''''''''''''''
' NOTE: old form of IF allowed linenumber directly 
' after THEN and ELSE, but had to all be on one line.
' Otherwise the GOTO linenumber and GOSUB linenumber 
' processing would handle the IF THEN ELSE statement.
'''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Function ProcessKeyWord$(pPtr, pStatement$, pKw$) 
 
Local ptr, updatedPtr
 
updatedPtr = pPtr ' return value
 
' move past space/s
ptr = MovePtrPastSpaces(pStatement$, pPtr)
 
params = parse(pStatement$, ptr)
 
if pKw$ = "THEN" OR pKw$ = "ELSE" then ' for obsolete form - Then or Else immediately followed by line num or label
  
if bDebug then ? #9, "Entering THEN or ELSE processing with keyword ";pKw$; " and param ";param$(1)
 
 P1$ = param$(1)
  endP1Ptr = paramPtr(1) + LEN(param$(1))
  if params = 0 then ' it's continued on the next line, so it can't be the obsolete form
    updatedStatement$ = pStatement$ ' so leave it as it is and the GOTO or GOSUB processing will handle it
    updatedPtr = LEN(Statement$)
  elseif ( UCASE$(P1$) = "GOTO" ) OR ( UCASE$(P1$) = "GOSUB" ) then
    ' "Next term is GOTO or GOSUB, so leaving pointer and statement as is"
    updatedStatement$ = pStatement$ ' gosub and goto processing will handle it
  elseif VAL(P1$) = 0 Then ' a label
    ' Next term is a label, so leaving it as is
    updatedStatement$ = pStatement$
    updatedPtr = endP1Ptr
  else
    ' "Updating obsolete form of Then or Else where the line numbr immediately follows the keyword (ie, no GOTO)"
    newNumber$ = LookupLine$(P1$)
    updatedStatement$ = Left$(pStatement$, paramPtr(1) - 1) + newNumber$ + MID$(pStatement$, endP1Ptr)
    updatedPtr = paramPtr(1) + LEN(newNumber$)
    if bDebug then 
      ? #9, "Updated Statement is >>>";updatedStatement$;"<<<"
      ? #9, "Balance of string is >>>";mid$(updatedStatement$, updatedptr);"<<<"
      ? #9, "New pointer is ";updatedPtr
    endif
  endif  
     
elseif pKw$ = "GOTO" OR pKw$ = "GOSUB" then 
 
  if bDebug then ? #9, "Entering GOTO or GOSUB processing with keyword ";pKw$; " and param ";param$(1)
 
  ProcessMultipleTargets(pStatement$, ptr, params)
  updatedStatement$ = pStatement$
  updatedPtr = ptr ' pass back up the call chain
 
elseif pKw$ = "SETPIN" then  ' param 3 optional
   
  if params = 2 then
    updatedStatement$ = pStatement$ ' doesn't use optional label or line number
    updatedPtr = paramPtr(2) + LEN(param$(2))
  else
    P3$ = param$(3) ' a label or line number
    endP3Ptr = paramPtr(3) + LEN(P3$)
    if VAL(P3$) = 0 then
      updatedStatement$ = pStatement$ ' a label
    else
      newNumber$ = LookupLine$(P3$)
      updatedStatement$ = Left$(pStatement$, paramPtr(3) - 1) + newNumber$ + MID$(pStatement$, endP3Ptr)
    endif
    updatedPtr = paramPtr(3) + LEN(newNumber$)
  endif 
     
elseif pKw$ = "SETTICK" then ' param 2 mandatory
' do not change if target (param 2) is 0, ie settick 0,0 but otherwise change the second parameter 
   
  if params = 2 then
    P2$ = param$(2)
    endP2Ptr = paramPtr(2) + LEN(P2$)
    if P2$ = "0" then
      updatedStatement$ = pStatement$ ' reset of SetTick interrupt.  Don't change!
    elseif VAL(P2$) = 0 then
      updatedStatement$ = pStatement$ ' a label
    else
      newNumber$ = LookupLine$(P2$)
      updatedStatement$ = Left$(pStatement$, paramPtr(2) - 1) + newNumber$ + MID$(pStatement$, endP2Ptr)
    endif
    updatedPtr = paramPtr(2) + LEN(newNumber$)
  else
    ? "Malformed SETTICK command: " + pStatement$
    INPUT "Press Ctrl-C to stop, any other key to continue "; In$
  endif
else 
  "You should never get this message.  The program tried to process the unknown keyword "; pKw$
  Input "Press Ctrl-C to break, or any other key to continue"; In$
endif
 
pPtr = updatedPtr
ProcessKeyWord$ = updatedStatement$    
  
End Function  ' ProcessKeyWord$
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Subroutine 
' This subroutine processes 1 or more labels or linenumbers separated by commas 
' and optional spaces following a GOTO or GOSUB keyword, and it also processes 
' the normal single label / linenumber calls like if x = 3 then goto 10 and, through 2
' separate calls to this subroutine, calls like If x = 3 then goto 10 else goto 20.
'
' enters the routine with the pointer pointing at the first character of the first token
' after the keyword, and the Param$ array holds the token values.
' Returns two values, updated pStatement$ and ptr.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Sub ProcessMultipleTargets(pStatement$, ptr, params)
 
Local updatedStatement$, P$, n, newNumber$, bExit
 
updatedStatement$ = Left$(pStatement$, ptr - 1) ' start with everything up to the start of first linenum/label
bExit = False
n = 1
 
if bDebug then ? #9, "In ProcessMultipleStatements with pointer ";ptr; " and "; params ; " parameters"
 
DO While (n <= params) AND (NOT bExit)
  P$ = param$(n) 
  PLen = LEN(P$) 
  endPPtr = paramPtr(n) + PLen - 1
  if n < params then
    separator$ = mid$(pStatement$, endPPtr + 1, paramPtr(n+1) - endPPtr - 1)
  else
    if endPPtr = LEN(pStatement$) then ' for example, if the last token is a single character like :, there's nothing following it
      separator$ = ""
    else
      separator$ = mid$(pStatement$, endPPtr + 1)
    endif
  endif
  if VAL(P$) = 0 Then  ' a label
    updatedStatement$ = updatedStatement$ + P$ + separator$
  else
    newNumber$ = LookupLine$(P$)
    updatedStatement$ = updatedStatement$ + newNumber$ + separator$   
  endif
 
  UCP$ = UCASE$(param$(n+1))
  if UCP$ = "ELSE" then 
    bExit = True
  endif
  n = n + 1
Loop
ptr = endPPtr
pStatement$ = updatedStatement$   
End Sub   ' ProcessMultipleTargets

'
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Function
' Parse the string into tokens based on spaces and commas
' as separators.  Store the tokens in the global Param$ array.
' Store the offset within the original line of the start of 
' each token into another global array, ParamPtr.
'
' Returns the number of parameters in the array
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function Parse(pStatement$, pPtr) 
Local i, n, nextChar$
 
' Clear the array that will hold the parameters
For i = 1 to MaxParams
  Param$(i) = ""
Next i
 
StatementLen = Len(pStatement$)
 
ParamNum = 0  
n = pPtr
' skip any leading spaces
NextChar$ = mid$(pStatement$, n, 1)
Do While NextChar$ = " "
  n = n + 1
  NextChar$ = mid$(pStatement$, n, 1)
Loop
 
' At this point we have skipped all the spaces (if any) after the keyword and are pointing at the first parameter
 
Do While n <= StatementLen
  Parameter$ = ""
  ParamNum = ParamNum + 1
  ParamPtr(ParamNum) = n
   
  Do While (NextChar$ <> " ") AND (NextChar$ <> ",") AND (n <= StatementLen)
    Parameter$ = Parameter$ + NextChar$
        n = n + 1
        NextChar$ = mid$(pStatement$, n, 1)
  Loop
   
  Param$(ParamNum) = Parameter$

  Do While ((NextChar$ = " ") OR (NextChar$ = ",")) AND (n <= StatementLen)
     n = n + 1
     NextChar$ = mid$(pStatement$, n, 1)
  Loop
Loop
 
if bDebug then
  for i = 1 to ParamNum
    ? #9, "Param ";i; " is "; Param$(i)
  next i
endif
 
Parse = ParamNum
end Function   ' Parse
'
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Function
' Search the "lines" array to find the original line number.
' Return the value of the new line number.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Function LookupLine$(OldLine$)
Local bFound, i, OldLine
 
bFound = False
i = 1
OldLine = VAL(OldLine$)
Do While (i <= LinesInFile) AND NOT bFound
   IF lines(i, 1) = OldLine THEN 
     bFound = True  
   Else
     i = i + 1
   Endif
Loop
 
if NOT bFound then
  PRINT : PRINT "No matching line number " + NextNumber$ + " found in original program."
  End  
Endif
 
if bDebug Then
  LookupLine$ = "***" + STR$(first + ((i - 1) * increment)) 'the *** is to ensure original and new are different lengths, and identify unprocessed linenums
else
  LookupLine$ = STR$(first + ((i - 1) * increment))
endif  
   
End Function   ' LookupLine$
6370 '
6380 '''''''''''''''''''''''''''''''''''''''''''''''''''''''
6390 'Function
6400 'Return the number of lines in the input file.
6410 '''''''''''''''''''''''''''''''''''''''''''''''''''''''
6420 'Note: Closes and reopens file so input pointer reset to start.
6430 '''''''''''''''''''''''''''''''''''''''''''''''''''''''
6440 Function CountLines(FileName$)
6450 local Count
6460 '
6470 OPEN FileName$ for INPUT as #3
6480 Count = 0
6490 DO WHILE NOT EOF(#3)
6500     Line Input #3, inp$
6510     Count = Count + 1
6520 LOOP
6530 Close #3
6540 
6550 CountLines = Count
6560 End Function   ' CountLines
6570 '
6580 '''''''''''''''''''''''''''''''''''''''''''''''''''''
6590 'Function
6600 'scan the line and move the pointer to the first 
6610 'character past any leading spaces
6620 '
6630 '''''''''''''''''''''''''''''''''''''''''''''''''''''
6640 ' 
6650 Function MovePtrPastSpaces(pStr$, pPtr)
6660 local ptr
6670
6680 ptr = pPtr
6690 DO WHILE mid$(pStr$,ptr,1) = " "
6700   ptr = ptr + 1
6710 LOOP
6720 MovePtrPastSpaces = ptr
6730 End Function   ' MovePtrPastSpaces
6740 '
6750 '''''''''''''''''''''''''''''''''''''''''''''''''''''
6760 'Function
6770 'scan the line and move the pointer to the first 
6780 'character past any leading spaces
6790 'note: function also updates LineNum parameter
6800 '''''''''''''''''''''''''''''''''''''''''''''''''''''
6810 ' 
6820 Function MovePtrPastLineNumber(pStr$, pPtr, LineNum)
6830 local ptr, nbrStr$
6840
6850 ptr=pPtr
6860 ' then get the number (if there)  
6870 nbrstr$ = ""  
6880 ch$ = MID$(pStr$,ptr,1)
6890 DO WHILE ch$ >= "0" AND ch$ <= "9"  
6900   ptr = ptr + 1
6910   nbrstr$ = nbrstr$ + ch$
6920   ch$ = MID$(pStr$,ptr,1)
6930 LOOP 
6940 LineNum = Val(nbrstr$) 
6950 MovePtrPastLineNumber = ptr
6960 End Function   ' MovePtrPastLineNumber
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Function
' Check whether the pointer is within a pair of double
' quotes.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Note: needed to ensure we do not incorrectly see a colon,
' REM or single quote as being meaningful to the code
' when they are actually just part of an instruction
' such as THEN Y$ = "This is a colon:, a quote ', or REM "
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Function InDoubleQuotes(Posn, UCLine$)
Local bInQuotes, StartPosn, EndPosn
 
bInQuotes = False
StartPosn = INSTR(1, UCLine$, chr$(34))
EndPosn = INSTR(StartPosn + 1, UCLine$, chr$(34))
 
DO While (StartPosn > 0) AND (EndPosn > 0) AND (bInQuotes = False)
  if (Posn > StartPosn) AND (Posn < EndPosn) Then
     bInQuotes = True
  else
    StartPosn = INSTR(EndPosn + 1, UCLine$, chr$(34))
    EndPosn = INSTR(StartPosn + 1, UCLine$, chr$(34))  
  endif
Loop
 
InDoubleQuotes = bInQuotes
End Function   ' InDoubleQuotes
 
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Function
' Get the next REM on the line
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' REM can occur at the beginning of a line with nothing before it.
' REM can occur immediately after a colon.
' Other than that, it must be preceded by a space.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Function GetNextREM(Posn, UCLine$)
Local i, bFound, REMPosn
 
REMPosn = 0
i = INSTR(Posn, UCLine$, "REM ")
 
DO While (REMPosn = 0) AND (i > 0)
  if i = 1 then 'start of line 'REM '
    REMPosn = i
  else
    if MID$(UCLine$, i-1, 1) = " " then ' REM '
      REMPosn = i
    else
      if MID$(UCLine$,i-1, 1) = ":" then ':REM '
        REMPosn = i
      endif
    endif
  endif
  i = INSTR(i + 1, UCLine$, "REM ")
LOOP
 
GetNextREM = REMPosn
End Function   ' GetNextREM

'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Subroutine
' This subroutine reads the user preferences from the configuration
' file.  If the file does not exist, it is created.
' 
' We need two different ways of testing for the file's existance
' because MMBasic only returns valid Error Numbers for files on the
' A: or B: drives.  It does not return a useful error number if 
' the file does not exist in DOS.
' 
' The Config file is the name of the calling program - .BAS + .CFG
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Sub ReadConfigFile()
 
Local ConfFile$
? "Entering ReadConfigFile"
' Open the Config file if it exists.  If it doesn't exist, create it.  
ConfFile$ = LEFT$(MM.FNAME$, LEN(MM.FNAME$) - 4) + ".cfg" ' replace .bas with .cfg
? "Conf file is "; ConfFile$
if FileExists(ConfFile$) then
  ? "File ";ConfFile$; " Exists"
  ' Read in the values - allocate to global variables
   
   
else
  ? "File ";ConfFile$; " does NOT exist"
  ' Prompt for the values and create the config file
  
   
endif

 
End Sub ' ReadConfigFile
 
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Function
' Check whether a file exists.
' Returns a Boolean True or False.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Uses two different methods to check, depending on whether
' the code is run under DOS, or on  Maximite.
' I've put in 2 bug reports, and this code will not work 
' they are resolved.  MM.DEVICE$ returns "" under DOS, and
' the SYSTEM command seems to no longer work under DOS.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Function fileExists(FileName$)

Local OutString$, FileRead$

? "Running under "; MM.DEVICE$
if MM.DEVICE$ = "DOS" then
  ? "Running under DOS"
  open "RunMe.bat" for output as #7
  OutString$ = "DIR "+ chr$(34) + FileName$ + chr$(34) + " /O:N /B >temp.dat"
  ? "About to write DIR command to batch file: " + OutString$ 
  Print #7, OutString$ 
  Print #7, "Exit" 
  Close #7
  ' Run batch that redirects DIR results to temp.dat.
  ' Redirect all messages from the batch file to nul (ie, not displayed)
  SYSTEM "RunMe.bat > nul"   

  'Now read the file name from the temp.dat file.  If nothing there, it doesn't exist.
  Open "temp.dat" for input as #7
  Line Input #7, FileRead$
  bFileExists$ = (FileRead$ <> "")
  Close #7
  ' KILL "RunMe.bat"  ' and clean up 
  ' KILL "temp.dat"  
else         
' Turn off Abort on Error so we can capture the Error Number
  Option Error Continue
  Open FileName$ for Input as #7
  ' ? "File name is "; FileName$; " Error Number is "; MM.ERRNO      
  If MM.ERRNO = 6 then ' can't find file
    ? "File doesn't exist"
    bFileExists = False
  elseif MM.ERRNO = 0 then
    bFileExists = True
  else
    ? "ERROR: the Existence Check for file "; ConfFile$; " returned MM.ERRNO "; MM.ERRNO
    ? "The description of MM.ERRNO"; MM.ERRNO; " is: "; ErrNoDesc$(MM.ERRNO)
    Close #7
    Option Error Abort
    END
  Endif
  Close #7
  Option Error Abort

endif

fileExists = bFileExists
 
End Function ' fileExists

'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Function
' Return the matching Description for an MMBasic 
' Error Number.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' The descriptions  were taken from the V4.3 manual.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'       
Function ErrNoDesc$(ErrNo)
  
Local Desc$
 
if ErrNo = 0 then
  Desc$ = "No error"
elseif ErrNo = 1 then
  Desc$ = "No SD card found"
elseif ErrNo = 2 then
  Desc$ = "SD card is write protected"
elseif ErrNo = 3 then
  Desc$ = "Not enough space"
elseif ErrNo = 4 then
  Desc$ = "All root directory entries are taken"
elseif ErrNo = 5 then
  Desc$ = "Invalid filename"
elseif ErrNo = 6 then
  Desc$ = "Cannot find file"
elseif ErrNo = 7 then
  Desc$ = "Cannot find directory"
elseif ErrNo = 8 then
  Desc$ = "File is read only"
elseif ErrNo = 9 then
  Desc$ = "Cannot open file"
elseif ErrNo = 10 then
  Desc$ = "Error reading from file"
elseif ErrNo = 11 then
  Desc$ = "Error writing to file"
elseif ErrNo = 12 then
  Desc$ = "Not a file"
elseif ErrNo = 13 then
  Desc$ = "Not a directory"
elseif ErrNo = 14 then
  Desc$ = "Directory not empty"
elseif ErrNo = 15 then
  Desc$ = "Hardware error accessing the storage media"
elseif ErrNo = 16 then
  Desc$ = "Flash memory write failure"
else
  Desc$ = "******** " + ErrNo + " is an invalid Error Number iaw V4.3 manual **********"
endif        
 
ErrNoDesc$ = Desc$
End Function ' ErrNoDesc$